package vlc_besturen;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author Koen Bollen <k.bollen@hva.nl>, 2010
 */
public final class VLCv2 extends Thread {

    public enum PlayState {

        STOPPED,
        PLAYING,
        PAUSED
    }
    public static final int MAX_RETRIES = 5;
    private static final Pattern rx_status = Pattern.compile("status change: \\((.+?): (.+?)\\)(:(.*))?");
    public final String host;
    public final int port;
    private Socket sock;
    private BufferedReader in;
    private BufferedOutputStream out;
    private boolean connected;
    private boolean verbose;
    private long timeout;
    private final List<StatusListener> listeners;
    private final LinkedBlockingQueue<String> output;
    public final Controller rc;
    public final Information info;
    private PlayState state;

    public VLCv2(String host, int port, boolean verbose) {
        this.host = host;
        this.port = port;
        this.verbose = verbose;
        this.connected = false;
        this.timeout = 1000;

        this.listeners = new LinkedList<StatusListener>();
        this.output = new LinkedBlockingQueue<String>();

        this.rc = new Controller(this);
        this.info = new Information(this);
        this.state = PlayState.STOPPED;

        // Thread settings:
        this.setName("VLC Connection");
        this.setDaemon(true);
    }

    public VLCv2(String host, int port) {
        this(host, port, false);
    }

    /**
     * Connect to VLC. It retries {MAX_RETRIES} times. When connected it causes
     * VLC to stop, defaulting to a STOPPED state.
     */
    public void connect() {
        if (this.connected) {
            return;
        }
        int retries = VLCv2.MAX_RETRIES;
        while (retries > 0) {
            if (this.verbose) {
                System.out.println("Connecting to VLC on " + this.host + ":" + this.port + "...");
            }
            try {
                this.sock = new Socket(this.host, this.port);
                this.in = new BufferedReader(new InputStreamReader(this.sock.getInputStream()));
                this.out = new BufferedOutputStream(this.sock.getOutputStream());
                break;
            } catch (IOException ex) {
                System.err.println("Unable to connect to VLC: " + ex.getMessage());
                if (retries > 0) {
                    retries--;
                    System.err.println("Retrying...");
                    try {
                        Thread.sleep((VLCv2.MAX_RETRIES - retries) * 1000);
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
        if (retries == 0) {
            System.out.println("Failed");
            System.exit(1);
        }
        if (this.verbose) {
            System.out.println("Connected!");
        }
        this.connected = true;

        // Default to the STOPPED state so it's possible to keep track of
        // the states:
        this.command("stop");
        this.command("pause");
        this.command("stop");
        this.state = PlayState.STOPPED;

        super.start();
    }

    /**
     * Disconnects from VLC.
     */
    public void disconnect() {
        if (!this.connected) {
            return;
        }

        if (this.verbose) {
            System.out.println("Closing connection to VLC...");
        }

        this.interrupt();

        //this.command("quit");

        try {
            this.out.close();
        } catch (IOException ex) {
        }

        try {
            this.in.close();
        } catch (IOException ex) {
        }

        try {
            this.sock.close();
        } catch (IOException ex) {
        }

        try {
            this.join();
        } catch (InterruptedException ex) {
        }

        if (this.verbose) {
            System.out.println("Disconnected.");
        }
    }

    private boolean checkconncetion() {
        if (!this.connected) {
            System.err.println("VLCv2 not connected! Call the connect() method first!");
            return false;
        }
        return true;
    }

    @Override
    public void start() {
        throw new RuntimeException("Don't start this thread directly, use the connect() method instead.");
    }

    @Override
    public void run() {
        if (!this.connected) {
            System.err.println("VLC is not connected, call the connect() method first.");
            System.exit(1);
        }
        while (this.connected && !this.isInterrupted()) {
            String line = null;
            try {
                line = this.in.readLine();
            } catch (IOException e) {
                this.checkconncetion();
                continue;
            }
            if (line == null) {
                continue;
            }

            if (this.verbose) {
                System.out.println("< " + line.trim());
            }

            // Test if this line was an "status change:" line.
            Matcher matcher = rx_status.matcher(line.trim());
            if (matcher.find()) {
                String name = matcher.group(1).trim();
                String value = matcher.group(2).trim();
                String message = matcher.group(3);
                if (message != null) {
                    message = message.trim();
                }
                fireEvent(name, value, message);
                continue;
            }

            // Normal output:
            output.offer(line.trim());
        }
    }

    private void fireEvent(String key, String value, String message) {
        LinkedList<StatusListener> copy = new LinkedList<StatusListener>();
        synchronized (this.listeners) {
            copy.addAll(this.listeners);
        }
        for (StatusListener l : copy) {
            l.onStatusChanged(this, key, value, message);
        }
    }

    public void command(String command) {
        if (!this.checkconncetion()) {
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex) {
        }
        command = command.trim() + "\n";
        if (this.verbose) {
            System.out.println("> " + command.trim());
        }
        try {
            this.out.write(command.getBytes(), 0, command.length());
            this.out.flush();
        } catch (IOException e) {
            System.err.println("Failed to send command: " + e.getMessage());
            this.checkconncetion();
        }
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public void addStatusListener(StatusListener l) {
        synchronized (this.listeners) {
            if (!this.listeners.contains(l)) {
                this.listeners.add(l);
            }
        }
    }

    public void removeStatusListener(StatusListener l) {
        synchronized (this.listeners) {
            this.listeners.remove(l);
        }
    }

    public PlayState getPlayState() {
        return this.state;
    }

    public interface StatusListener {

        public void onStatusChanged(VLCv2 vlc, String key, String value, String message);
    }

    public class Controller {

        private VLCv2 vlc;

        private Controller(VLCv2 vlc) {
            this.vlc = vlc;
        }

        /**
         * Start playback.
         *
         * @return
         */
        public void play() {
            switch (this.vlc.state) {
                case PAUSED:
                    this.vlc.command("pause");
                    this.vlc.state = PlayState.PLAYING;
                    break;
                case STOPPED:
                    this.vlc.command("play");
                    this.vlc.state = PlayState.PLAYING;
                    break;
            }
        }

        /**
         * Toggle playback between pause and play.
         * @return
         */
        public void pause() {
            switch (this.vlc.state) {
                case PAUSED:
                    this.vlc.command("pause");
                    this.vlc.state = PlayState.PLAYING;
                    break;
                case PLAYING:
                    this.vlc.command("pause");
                    this.vlc.state = PlayState.PAUSED;
                    break;
                case STOPPED:
                    this.vlc.command("play");
                    this.vlc.state = PlayState.PLAYING;
                    break;
            }
        }

        /**
         * Stop playback.
         *
         * @return
         */
        public void stop() {
            switch (this.vlc.state) {
                case PAUSED:
                    this.vlc.command("pause");
                    this.vlc.command("stop");
                    break;
                case PLAYING:
                    this.vlc.command("stop");
                    break;
            }
            this.vlc.state = PlayState.STOPPED;
        }

        /**
         * Select the next entry in the playlist.
         */
        public void next() {
            this.vlc.command("next");
        }

        /**
         * Select the previous entry in the playlist.
         */
        public void prev() {
            this.vlc.command("prev");
        }

        /**
         * Set VLC to fullscreen.
         */
        public void fullscreen() {
            this.vlc.command("f on");
        }

        /**
         * Clear the current playlist.
         *
         * @return 0 on success
         */
        public void clear() {
            this.vlc.command("stop");
            this.vlc.command("pause");
            this.vlc.command("stop");
            this.vlc.command("clear");
            this.vlc.state = PlayState.STOPPED;
        }

        /**
         * Seek playback to a point in the media.
         *
         * @param seconds The number of seconds to seek to.
         */
        public void seek(int seconds) {
            if (seconds >= 0) {
                this.vlc.command("seek " + seconds);
            }
        }

        /**
         * Enqueue a file to the playlist.
         *
         * @param path the path/filename/url of to enque.
         * @return 0 on success.
         */
        public void enqueue(String path) {
            this.vlc.command("enqueue " + path.trim());
        }

        /**
         * Request all statuses, they are send to the {VLCv2.StatusListener}s.
         */
        public void status() {
            this.vlc.command("status");
        }
    }

    public class Information implements StatusListener {

        private VLCv2 vlc;

        private Information(VLCv2 vlc) {
            this.vlc = vlc;
        }

        private String get(String command) {
            synchronized (this.vlc.output) {
                String result = null;
                this.vlc.output.clear();
                this.vlc.command(command);
                try {
                    result = this.vlc.output.poll(this.vlc.timeout, TimeUnit.MILLISECONDS);
                } catch (InterruptedException ex) {
                }
                if (result == null) {
                    this.vlc.checkconncetion();
                }
                return result;
            }
        }

        private int getInt(String command) {
            try {
                return Integer.parseInt(this.get(command));
            } catch (NumberFormatException ex) {
                return -1;
            }
        }

        public void onStatusChanged(VLCv2 vlc, String key, String value, String message) {
        }

        /**
         * Return the current time of the playing item.
         *
         * @return The time.
         */
        public int getTime() {
            sleep(500);
            return this.getInt("get_time");
        }

        /**
         * Return the total length of the currently playing item.
         */
        public int getLength() {
            sleep(500);
            return this.getInt("get_length");
        }

        /**
         * Return the title/filename of the current item.
         *
         * @return
         */
        public String getTitle() {
            sleep(500);
            return this.get("get_title");
        }
    }

    /**
     * This method sleeps for a given number of milliseconds.
     */
    public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) {
            System.err.println("Exception in sleep: " + ex.getMessage());
        }
    }
}
